readline: improve performance#64585
Conversation
Speed up Interface construction: - Hoist the history accessor property descriptors to module scope and define them with a single ObjectDefineProperties call, instead of allocating six closures and four descriptor objects per instance. - Stop assigning the history options onto the input stream. This avoids hidden class transitions on the user provided stream and no longer mutates it observably. - Only check process.env.TERM for a dumb terminal when the interface is in terminal mode. Reading process.env goes through the environment interceptor and is comparatively expensive, and _ttyWrite is never called when terminal is false. Signed-off-by: Matteo Collina <hello@matteocollina.com>
|
Review requested:
|
Replace the per-line regular expression exec loop with a single split pass, and use a plain string split on "\n" when the chunk contains none of the rare line endings (\r, \u2028, \u2029). This avoids allocating a match object and two index entries per line and lets the common case use the fast string split path. readline/readline-iterable.js type='new' improves by up to 80% and the event based line consumption by up to 65%. Signed-off-by: Matteo Collina <hello@matteocollina.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #64585 +/- ##
==========================================
- Coverage 90.13% 90.12% -0.02%
==========================================
Files 741 741
Lines 241976 241994 +18
Branches 45543 45551 +8
==========================================
- Hits 218116 218101 -15
- Misses 15379 15410 +31
- Partials 8481 8483 +2
🚀 New features to boost your workflow:
|
This comment was marked as outdated.
This comment was marked as outdated.
Commit Queue failed- Loading data for nodejs/node/pull/64585 ✔ Done loading data for nodejs/node/pull/64585 ----------------------------------- PR info ------------------------------------ Title readline: improve performance (#64585) Author Matteo Collina <matteo.collina@gmail.com> (@mcollina) Branch mcollina:readline-createinterface-perf -> nodejs:main Labels readline, author ready, needs-ci Commits 2 - readline: reduce createInterface overhead - readline: speed up line splitting in normal write Committers 1 - Matteo Collina <hello@matteocollina.com> PR-URL: https://github.com/nodejs/node/pull/64585 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> ------------------------------ Generated metadata ------------------------------ PR-URL: https://github.com/nodejs/node/pull/64585 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> -------------------------------------------------------------------------------- ℹ This PR was created on Sat, 18 Jul 2026 21:45:57 GMT ✔ Approvals: 5 ✔ - James M Snell (@jasnell) (TSC): https://github.com/nodejs/node/pull/64585#pullrequestreview-4731050691 ✔ - Rafael Gonzaga (@RafaelGSS) (TSC): https://github.com/nodejs/node/pull/64585#pullrequestreview-4731061804 ✔ - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/64585#pullrequestreview-4731326110 ✔ - Benjamin Gruenbaum (@benjamingr) (TSC): https://github.com/nodejs/node/pull/64585#pullrequestreview-4731378980 ✔ - Gürgün Dayıoğlu (@gurgunday): https://github.com/nodejs/node/pull/64585#pullrequestreview-4734487326 ✔ Last GitHub CI successful ℹ Last Full PR CI on 2026-07-20T14:46:25Z: https://ci.nodejs.org/job/node-test-pull-request/74905/ - Querying data for job/node-test-pull-request/74905/ ✔ Build data downloaded ✔ Last Jenkins CI successful -------------------------------------------------------------------------------- ✔ No git cherry-pick in progress ✔ No git am in progress ✔ No git rebase in progress -------------------------------------------------------------------------------- - Bringing origin/main up to date... From https://github.com/nodejs/node * branch main -> FETCH_HEAD ✔ origin/main is now up-to-date - Downloading patch for 64585 From https://github.com/nodejs/node * branch refs/pull/64585/merge -> FETCH_HEAD ✔ Fetched commits as c558c8e81a7c..0fd92a96ca87 -------------------------------------------------------------------------------- [main 5fe0cee5e7] readline: reduce createInterface overhead Author: Matteo Collina <hello@matteocollina.com> Date: Sat Jul 18 23:45:16 2026 +0200 3 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 benchmark/readline/readline-createInterface.js [main 07a3f277aa] readline: speed up line splitting in normal write Author: Matteo Collina <hello@matteocollina.com> Date: Sun Jul 19 00:03:28 2026 +0200 1 file changed, 34 insertions(+), 26 deletions(-) ✔ Patches applied There are 2 commits in the PR. Attempting autorebase. (node:352) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated. (Use `node --trace-deprecation ...` to show where the warning was created) Rebasing (2/4) Executing: git node land --amend --yes --------------------------------- New Message ---------------------------------- readline: reduce createInterface overhead
Signed-off-by: Matteo Collina <hello@matteocollina.com>
|
Speed up Interface construction: - Hoist the history accessor property descriptors to module scope and define them with a single ObjectDefineProperties call, instead of allocating six closures and four descriptor objects per instance. - Stop assigning the history options onto the input stream. This avoids hidden class transitions on the user provided stream and no longer mutates it observably. - Only check process.env.TERM for a dumb terminal when the interface is in terminal mode. Reading process.env goes through the environment interceptor and is comparatively expensive, and _ttyWrite is never called when terminal is false. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #64585 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
|
Landed in 9041ad0 |
Speed up
readline.createInterface(), which is on the hot path of every CLI tool that reads lines from a stream.A CPU profile of interface construction showed three avoidable costs:
setupHistoryManager()accounted for ~43% of construction self time: every instance ran fourObjectDefinePropertycalls, allocating six fresh closures and four descriptor objects each time. The accessors (history,historyIndex,historySize,isFlushing) are now defined from a module-level shared descriptor map with a singleObjectDefinePropertiescall. They remain own, enumerable, configurable properties, so observable semantics are unchanged.size,historyandremoveHistoryDuplicatesonto the user-provided input stream to forward them to the history manager. It now passes a plain options object instead, so the user's stream is no longer observably mutated and doesn't go through extra hidden-class transitions. The legacy positional-arguments path is unchanged.Interfacewrapper unconditionally readprocess.env.TERM(which goes through the env interceptor) to bind the dumb-terminal_ttyWrite. The check is now gated onthis.terminal, since_ttyWriteis never invoked when the interface is not in terminal mode.Results with the new benchmark (30 runs, interleaved via
benchmark/compare.js):Update: a second commit speeds up line throughput as well.
[kNormalWrite]ran a regular-expressionexecloop that allocated a match object and two index entries per line, then sliced each line out in JS. It now does a single split pass, and uses a plain string split on"\n"when the chunk contains none of the rare line endings (\r,,), which takes V8's fast string-split path.Results for
readline/readline-iterable.js(20 runs, interleaved):Event-based consumption (
rl.on('line')) goes from 8.1M to 13.3M lines/s (+65%) in a microbenchmark of 32-byte lines.